/*
 * Action.java
 *
 *  
 */
package org.msb.finance.app;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.*;
import javax.swing.event.ChangeEvent;

/**
 * The {@code Action} class extends the {@link AbstractAction} class to provide enhancements to its usability. The
 * enhancements help reduce the amount of setup code for an {@link javax.swing.Action} object. The first enhancement is
 * the use of a delegate {@link ActionListener} object so that multiple {@code Action} instances can share a common
 * event handling object. The second enhancement is the use of a call back object ({@link ActionEnabledCallback}) to
 * which this instance delegates the responsibility of determining its enabled state. The third enhancement is a
 * constructor that takes an argument for each of the configurable properties, thereby allowing complete construction
 * and initialization of an instance in a single statement.
 * 
 * @author Marc Boudreau
 * 
 */
@SuppressWarnings("serial")
public class Action extends AbstractAction implements EnabledStateChangeListener {

	/*
	 * The actual implementation of the ActionListener interface. This allows multiple instances of this class to share
	 * the same ActionListener object.
	 */
	private ActionListener delegate;

	/*
	 * An object that will be called to determine what the state of the enabled property of this action should currently
	 * be.
	 */
	private ActionEnabledCallback callback;

	/**
	 * Constructs a new instance all of the properties initialized to their default values.
	 */
	public Action() {
		this(null);
	}

	/**
	 * Constructs a new instance with the provided {@link ActionListener} object as the delegate and all other
	 * properties initialized to their default values.
	 * 
	 * @param delegate
	 *            The {@link ActionListener} object to which this instance will delegate the handling of
	 *            {@link ActionEvent} objects.
	 */
	public Action(ActionListener delegate) {
		this.delegate = delegate;
	}

	/**
	 * Constructs a new instance with the provided parameters. Every configurable property can be set with this
	 * constructor.
	 * 
	 * @param delegate
	 *            The {@link ActionListener} object to which this instance will delegate the handling of
	 *            {@link ActionEvent} objects.
	 * @param name
	 *            A {@link String} object containing the text to set as the value for the name property.
	 * @param icon
	 *            An {@link Icon} object representing the image to set as the value for the small icon property.
	 * @param command
	 *            A {@link String} object containing the text to set as the value for the action command property.
	 * @param mnemonic
	 *            A {@code char} value to set as the value for the mnemonic property.
	 * @param mnemonicIndex
	 *            A {@code int} value to set as the value for the mnemonic index property.
	 * @param tooltip
	 *            A {@link String} object containing the text to set as the value for the short description property.
	 * @param accel
	 *            A {@link KeyStroke} object defining the keystroke to set as the value for the accelerator property.
	 * @param enabled
	 *            A {@code boolean} value to set as the value for the enabled property.
	 * @param selected
	 *            A {@code boolean} value to set as the value for the selected property.
	 * @param callback
	 *            A {@link ActionEnabledCallback} object to set as the call-back used to determine the dynamic state of
	 *            the enabled property. When the enabled property value is based on external factors.
	 */
	public Action(ActionListener delegate, String name, Icon icon, String command, char mnemonic, int mnemonicIndex,
			String tooltip, KeyStroke accel, boolean enabled, boolean selected, ActionEnabledCallback callback) {
		super(name, icon);

		putValue(ACCELERATOR_KEY, accel);
		putValue(ACTION_COMMAND_KEY, command);
		putValue(DISPLAYED_MNEMONIC_INDEX_KEY, new Integer(mnemonicIndex));
		putValue(MNEMONIC_KEY, new Integer(mnemonic));
		putValue(SELECTED_KEY, Boolean.valueOf(selected));
		putValue(SHORT_DESCRIPTION, tooltip);

		setEnabled(enabled);

		this.delegate = delegate;
		this.callback = callback;
	}

	/**
	 * Constructs a new {@code Action} object with the provided {@link Resources} object and the resource key prefix.
	 * The constructor will attempt to locate a text resource in the {@link Resources} with the key prefix itself. This
	 * will be parsed to obtain the Name, Mnemonic (and its index), and Accelerator key stroke. The {@link Resources}
	 * object will also be used to locate an {@link Icon} object for the Small Icon property and a text resource for the
	 * Short Description property.
	 * 
	 * @param delegate
	 *            A reference to an {@link ActionListener} object to which this instance will delegate the
	 *            {@link #actionPerformed(ActionEvent)} call.
	 * @param res
	 *            A {@link Resources} object from which some of the properties are obtained.
	 * @param keyPrefix
	 *            The common prefix for the keys retrieved from the {@link Resources} object.
	 * @param command
	 *            A {@link String} object containing the value to set as the Action Command property.
	 * @param enabled
	 *            A {@code boolean} value indicating the enabled state of this action.
	 * @param selected
	 *            A {@code boolean} value indicating if this action is selected (used by {@link JCheckBoxMenuItem} and
	 *            {@link JToggleButton}).
	 * @param callback
	 *            A {@link ActionEnabledCallback} object that is referred for the current enabled state of this action.
	 */
	public Action(ActionListener delegate, Resources res, String keyPrefix, String command, boolean enabled,
			boolean selected, ActionEnabledCallback callback) {
		super();

		StringBuilder caption = new StringBuilder(res.getString(keyPrefix));
		parseKeyStroke(caption);
		parseCaption(caption);

		putValue(SMALL_ICON, res.getIcon(keyPrefix + ".icon")); //$NON-NLS-1$
		putValue(ACTION_COMMAND_KEY, command);
		putValue(SELECTED_KEY, selected);
		putValue(SHORT_DESCRIPTION, res.getString(keyPrefix + ".tooltip")); //$NON-NLS-1$

		setEnabled(enabled);

		this.delegate = delegate;
		this.callback = callback;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
	 */
	public void actionPerformed(ActionEvent e) {
		if (null != this.delegate) {
			this.delegate.actionPerformed(e);
		}
	}

	/*
	 * Parses a compound String that can contain the values for the name, mnemonic, mnemonic index, and accelerator
	 * properties.
	 */
	void parseActionDefinition(String text) {
		StringBuilder buffer = new StringBuilder(text);

		parseKeyStroke(buffer);
		parseCaption(buffer);
	}

	/*
	 * Extracts the keystroke definition from the provided StringBuilder object. The keystroke definition (if specified)
	 * is found after the first unescaped '@' symbol. A double '@' symbol sequence is considered an escaped '@' symbol
	 * and is ignored by this function.
	 */
	private String extractKeyStrokeStr(StringBuilder text) {
		String keystroke = null;

		int start = 0;
		int pos = text.indexOf("@", start); //$NON-NLS-1$
		while (-1 != pos) {
			start = pos + 1;

			if (text.length() - 1 > pos) {
				/*
				 * Check if the next character is also an @ symbol.
				 */
				if ('@' == text.charAt(pos + 1)) {
					text.replace(pos, pos + 1, new String());
					start++;
				} else {
					keystroke = text.substring(pos + 1);
					break;
				}
			}

			pos = text.indexOf("@", start); //$NON-NLS-1$
		}

		if (-1 < pos) {
			text.delete(pos, text.length());
		}

		return keystroke;
	}

	/*
	 * Extracts the keystroke definition from the provided StringBuilder object and sets it as the value of the
	 * accelerator property.
	 */
	private void parseKeyStroke(StringBuilder text) {
		String keyStroke = extractKeyStrokeStr(text);

		putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(keyStroke));
	}

	/*
	 * Parses the contents of the provided StringBuilder object and sets the value of the name property and if
	 * specified, the value of the mnemonic and mnemonic index properties. The mnemonic and mnemonic index are specified
	 * by placing a '&' symbol immediately before the character to set as the mnemonic. Naturally, the index of that
	 * character (once the '&' is removed) is the value for the mnemonic index. A double '&' sequence is considered an
	 * escaped '&' and is reduced to a single '&' character is not considered as the mnemonic marker.
	 */
	private void parseCaption(StringBuilder text) {
		final String AMPERSAND = "&"; //$NON-NLS-1$

		char mnemonic = '\0';
		int index = -1;
		boolean found = false;

		int start = 0;
		while (text.length() > start) {
			int pos = text.indexOf(AMPERSAND, start);

			if (-1 == pos || text.length() - 1 == pos) {
				/*
				 * There wasn't an ampersand in the remainder of the string, or it was the last character of the string.
				 */
				start = text.length();
			} else {
				char c = text.charAt(pos + 1);
				text.replace(pos, pos + 1, new String());

				if ('&' == c) {
					start = pos + 1;
				} else {
					if (!found) {
						mnemonic = c;
						index = pos;

						found = true;
					}
				}
			}
		}

		putValue(NAME, text.toString());
		putValue(MNEMONIC_KEY, new Integer(mnemonic));
		putValue(DISPLAYED_MNEMONIC_INDEX_KEY, index);

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.event.ChangeListener#stateChanged(javax.swing.event.ChangeEvent)
	 */
	public void stateChanged(ChangeEvent e) {
		if (null != this.callback) {
			setEnabled(this.callback.isEnabled());
		}
	}
}
